<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    
    <title>reids分布式锁 | 王鹏的技术博客</title>
    
    
        <meta property="og:site_name" content="王鹏的技术博客">
    
    
        <meta property="article:author" content="王 鹏">
    
    
        <link rel="icon" href="/mhexo/img/favicon.ico">
    
    
<link rel="stylesheet" href="/mhexo/css/minireset.min.css">

    
<link rel="stylesheet" href="/mhexo/css/all.min.css">

    
<link rel="stylesheet" href="/mhexo/css/csshake.min.css">

    
<link rel="stylesheet" href="/mhexo/css/hljs/lioshi.css">

    
<link rel="stylesheet" href="/mhexo/css/jquery.fancybox.min.css">

    
<link rel="stylesheet" href="/mhexo/styl/main.css">

    
<script src="/mhexo/js/jquery.min.js"></script>

    
<script src="/mhexo/js/highlight.min.js"></script>

    
<script src="/mhexo/js/jquery.fancybox.min.js"></script>

    
<script src="/mhexo/js/clipboard.min.js"></script>

<meta name="generator" content="Hexo 5.2.0"></head>
<body>
    <header>
  <div class="outer">
    <div class="inner">
      <h1 class="logo-wrap">
        <a>王鹏的技术博客<b><sup>1.1</sup></b></a>
      </h1>
    </div>
    <div class="inner">
      <nav class="main-nav">
        
          <a href="/mhexo/">首页</a>
        
          <a href="/mhexo/archives">归档</a>
        
          <a href="/mhexo/categories">分类</a>
        
          <a href="/mhexo/tags">标签</a>
        
          <a href="/mhexo/friends">友链</a>
        
          <a href="/mhexo/about">关于</a>
        
          <a target="_blank" rel="noopener" href="https://www.baidu.com">百度</a>
        
      </nav>
    </div>
  </div>
</header>
    <div class="content">
        <section class="outer">
    <article>
        <div class="article-title">
    <h2>
        <a href="/mhexo/2020/12/22/reids%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/" class="shake shake-little" title="reids分布式锁">
            
            reids分布式锁
        </a>
    </h2>
    <div class="meta_box">
    
        
        
            
                
        
        <div class="meta meta_auth">
            <img src="/mhexo/img/default.png" alt="head" />
            <p>anonymous</p>
        </div>
    
        
        
        <div class="meta meta_date">
            <i class="fas fa-calendar-alt fa-fw" aria-hidden="true"></i>
            <p>发布于：2020年12月22日</p>
        </div>
    
        <div class="meta meta_update">
            <i class="fas fa-edit fa-fw" aria-hidden="true"></i>
            <p>更新于：2020年12月29日</p>
        </div>
    </div>
    

</div>

        <hr>
        <div class="article-entry">
            
            
            
            <h1 id="reids分布式锁"><a href="#reids分布式锁" class="headerlink" title="reids分布式锁"></a>reids分布式锁</h1><h2 id="什么是锁？"><a href="#什么是锁？" class="headerlink" title="什么是锁？"></a>什么是锁？</h2><ul>
<li>在单进程的系统中，当存在多个线程可以同时改变某个变量（可变共享变量）时，就需要对变量或代码块做同步，使其在修改这种变量时能够线性执行消除并发修改变量。</li>
<li>而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行，那么需要在某个地方做个标记，这个标记必须每个线程都能看到，当标记不存在时可以设置该标记，其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。</li>
<li>不同地方实现锁的方式也不一样，只要能满足所有线程都能看得到标记即可。如 Java 中 synchronize 是在对象头设置标记，Lock 接口的实现类基本上都只是某一个 volitile 修饰的 int 型变量其保证每个线程都能拥有对该 int 的可见性和原子修改，linux 内核中也是利用互斥量或信号量等内存数据做标记。</li>
<li>除了利用内存数据做锁其实任何互斥的都能做锁（只考虑互斥情况），如流水表中流水号与时间结合做幂等校验可以看作是一个不会释放的锁，或者使用某个文件是否存在作为锁等。只需要满足在对标记进行修改能保证原子性和内存可见性即可。</li>
</ul>
<h2 id="什么是分布式？"><a href="#什么是分布式？" class="headerlink" title="什么是分布式？"></a>什么是分布式？</h2><p>分布式的 CAP 理论告诉我们:</p>
<blockquote>
<p>任何一个分布式系统都无法同时满足一致性（Consistency）、可用性（Availability）和分区容错性（Partition tolerance），最多只能同时满足两项。</p>
</blockquote>
<p>目前很多大型网站及应用都是分布式部署的，分布式场景中的数据一致性问题一直是一个比较重要的话题。基于 CAP理论，很多系统在设计之初就要对这三者做出取舍。在互联网领域的绝大多数的场景中，都需要牺牲强一致性来换取系统的高可用性，系统往往只需要保证最终一致性。</p>
<h2 id="分布式场景"><a href="#分布式场景" class="headerlink" title="分布式场景"></a>分布式场景</h2><blockquote>
<p>此处主要指集群模式下，多个相同服务同时开启.</p>
</blockquote>
<p>在许多的场景中，我们为了保证数据的最终一致性，需要很多的技术方案来支持，比如<strong>分布式事务</strong>、<strong>分布式锁</strong>等。很多时候我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中，通过 Java 提供的并发 API 我们可以解决，但是在分布式环境下，就没有那么简单啦。</p>
<ul>
<li>分布式与单机情况下最大的不同在于其不是多线程而是<strong>多进程</strong>。</li>
<li>多线程由于可以共享堆内存，因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上，因此需要将标记存储在一个所有进程都能看到的地方。</li>
</ul>
<h2 id="什么是分布式锁？"><a href="#什么是分布式锁？" class="headerlink" title="什么是分布式锁？"></a>什么是分布式锁？</h2><ul>
<li>当在分布式模型下，数据只有一份（或有限制），此时需要利用锁的技术控制某一时刻修改数据的进程数。</li>
<li>与单机模式下的锁不仅需要保证进程可见，还需要考虑进程与锁之间的网络问题。（我觉得分布式情况下之所以问题变得复杂，主要就是需要考虑到<strong>网络的延时和不可靠</strong>。。。一个大坑）</li>
<li>分布式锁还是可以将标记存在内存，只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件等做锁与单机的实现是一样的，只要保证标记能互斥就行。</li>
</ul>
<h2 id="我们需要怎样的分布式锁？"><a href="#我们需要怎样的分布式锁？" class="headerlink" title="我们需要怎样的分布式锁？"></a>我们需要怎样的分布式锁？</h2><ul>
<li>可以保证在分布式部署的应用集群中，同一个方法在同一时间只能被一台机器上的一个线程执行。</li>
<li>这把锁要是一把可重入锁（避免死锁）</li>
<li>这把锁最好是一把阻塞锁（根据业务需求考虑要不要这条）</li>
<li>这把锁最好是一把公平锁（根据业务需求考虑要不要这条）</li>
<li>有高可用的获取锁和释放锁功能</li>
<li>获取锁和释放锁的性能要好</li>
</ul>
<h2 id="基于数据库做分布式锁"><a href="#基于数据库做分布式锁" class="headerlink" title="基于数据库做分布式锁"></a>基于数据库做分布式锁</h2><blockquote>
<p>基于乐观锁</p>
</blockquote>
<h2 id="基于表主键唯一做分布式锁"><a href="#基于表主键唯一做分布式锁" class="headerlink" title="基于表主键唯一做分布式锁"></a>基于表主键唯一做分布式锁</h2><p>利用主键唯一的特性，如果有多个请求同时提交到数据库的话，数据库会保证只有一个操作可以成功，那么我们就可以认为操作成功的那个线程获得了该方法的锁，当方法执行完毕之后，想要释放锁的话，删除这条数据库记录即可。</p>
<p>上面这种简单的实现有以下几个问题：</p>
<ul>
<li>这把锁强依赖数据库的可用性，数据库是一个单点，一旦数据库挂掉，会导致业务系统不可用。</li>
<li>这把锁没有失效时间，一旦解锁操作失败，就会导致锁记录一直在数据库中，其他线程无法再获得到锁。</li>
<li>这把锁只能是非阻塞的，因为数据的 insert 操作，一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列，要想再次获得锁就要再次触发获得锁操作。</li>
<li>这把锁是非重入的，同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。</li>
<li>这把锁是非公平锁，所有等待锁的线程凭运气去争夺锁。</li>
<li>在 MySQL 数据库中采用主键冲突防重，在大并发情况下有可能会造成锁表现象。</li>
</ul>
<p>当然，我们也可以有其他方式解决上面的问题。</p>
<ul>
<li>数据库是单点？搞两个数据库，数据之前双向同步，一旦挂掉快速切换到备库上。</li>
<li>没有失效时间？只要做一个定时任务，每隔一定时间把数据库中的超时数据清理一遍。</li>
<li>非阻塞的？搞一个 while 循环，直到 insert 成功再返回成功。</li>
<li>非重入的？在数据库表中加个字段，记录当前获得锁的机器的主机信息和线程信息，那么下次再获取锁的时候先查询数据库，如果当前机器的主机信息和线程信息在数据库可以查到的话，直接把锁分配给他就可以了。</li>
<li>非公平的？再建一张中间表，将等待锁的线程全记录下来，并根据创建时间排序，只有最先创建的允许获取锁。</li>
<li>比较好的办法是在程序中生产主键进行防重。</li>
</ul>
<h2 id="基于表字段版本号做分布式锁"><a href="#基于表字段版本号做分布式锁" class="headerlink" title="基于表字段版本号做分布式锁"></a>基于表字段版本号做分布式锁</h2><p>这个策略源于 mysql 的 mvcc 机制，使用这个策略其实本身没有什么问题，唯一的问题就是对数据表侵入较大，我们要为每个表设计一个版本号字段，然后写一条判断 sql 每次进行判断，增加了数据库操作的次数，在高并发的要求下，对数据库连接的开销也是无法忍受的。</p>
<blockquote>
<p>基于悲观锁</p>
</blockquote>
<h2 id="基于数据库排他锁做分布式锁"><a href="#基于数据库排他锁做分布式锁" class="headerlink" title="基于数据库排他锁做分布式锁"></a>基于数据库排他锁做分布式锁</h2><p>在查询语句后面增加<code>for update</code>，数据库会在查询过程中给数据库表增加排他锁 (注意： InnoDB 引擎在加锁的时候，只有通过索引进行检索的时候才会使用行级锁，否则会使用表级锁。这里我们希望使用行级锁，就要给要执行的方法字段名添加索引，值得注意的是，这个索引一定要创建成唯一索引，否则会出现多个重载方法之间无法同时被访问的问题。重载方法的话建议把参数类型也加上。)。当某条记录被加上排他锁之后，其他线程无法再在该行记录上增加排他锁。</p>
<p>我们可以认为获得排他锁的线程即可获得分布式锁，当获取到锁之后，可以执行方法的业务逻辑，执行完方法之后，通过<code>connection.commit()</code>操作来释放锁。</p>
<p>这种方法可以有效的解决上面提到的无法释放锁和阻塞锁的问题。</p>
<ul>
<li>阻塞锁？ <code>for update</code>语句会在执行成功后立即返回，在执行失败时一直处于阻塞状态，直到成功。</li>
<li>锁定之后服务宕机，无法释放？使用这种方式，服务宕机之后数据库会自己把锁释放掉。</li>
</ul>
<p>但是还是无法直接解决数据库单点和可重入问题。</p>
<p>这里还可能存在另外一个问题，虽然我们对方法字段名使用了唯一索引，并且显示使用 for update 来使用行级锁。但是，MySQL 会对查询进行优化，即便在条件中使用了索引字段，但是否使用索引来检索数据是由 MySQL 通过判断不同执行计划的代价来决定的，如果 MySQL 认为全表扫效率更高，比如对一些很小的表，它就不会使用索引，这种情况下 InnoDB 将使用表锁，而不是行锁。如果发生这种情况就悲剧了。。。</p>
<p>还有一个问题，就是我们要使用排他锁来进行分布式锁的 lock，那么一个排他锁长时间不提交，就会占用数据库连接。一旦类似的连接变得多了，就可能把数据库连接池撑爆。</p>
<h2 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h2><p><strong>优点</strong>：简单，易于理解</p>
<p><strong>缺点</strong>：会有各种各样的问题（操作数据库需要一定的开销，使用数据库的行级锁并不一定靠谱，性能不靠谱）</p>
<h2 id="基于-Redis-做分布式锁"><a href="#基于-Redis-做分布式锁" class="headerlink" title="基于 Redis 做分布式锁"></a>基于 Redis 做分布式锁</h2><h2 id="基于-redis-的-setnx-、expire-方法做分布式锁"><a href="#基于-redis-的-setnx-、expire-方法做分布式锁" class="headerlink" title="基于 redis 的 setnx()、expire() 方法做分布式锁"></a>基于 redis 的 setnx()、expire() 方法做分布式锁</h2><h2 id="setnx"><a href="#setnx" class="headerlink" title="setnx()"></a>setnx()</h2><p>setnx 的含义就是 SET if Not Exists，其主要有两个参数 setnx(key, value)。该方法是原子的，如果 key 不存在，则设置当前 key 成功，返回 1；如果当前 key 已经存在，则设置当前 key 失败，返回 0。</p>
<h2 id="expire"><a href="#expire" class="headerlink" title="expire()"></a>expire()</h2><p>expire 设置过期时间，要注意的是 setnx 命令不能设置 key 的超时时间，只能通过 expire() 来对 key 设置。</p>
<h2 id="使用步骤"><a href="#使用步骤" class="headerlink" title="使用步骤"></a>使用步骤</h2><p>1、setnx(lockkey, 1) 如果返回 0，则说明占位失败；如果返回 1，则说明占位成功</p>
<p>2、expire() 命令对 lockkey 设置超时时间，为的是避免死锁问题。</p>
<p>3、执行完业务代码后，可以通过 delete 命令删除 key。</p>
<p>这个方案其实是可以解决日常工作中的需求的，但从技术方案的探讨上来说，可能还有一些可以完善的地方。<strong>比如，如果在第一步 setnx 执行成功后，在 expire() 命令执行成功前，发生了宕机的现象，那么就依然会出现死锁的问题，所以如果要对其进行完善的话，可以使用 redis 的 setnx()、get() 和 getset() 方法来实现分布式锁。</strong></p>
<h2 id="基于-redis-的-setnx-、get-、getset-方法做分布式锁"><a href="#基于-redis-的-setnx-、get-、getset-方法做分布式锁" class="headerlink" title="基于 redis 的 setnx()、get()、getset()方法做分布式锁"></a>基于 redis 的 setnx()、get()、getset()方法做分布式锁</h2><p>这个方案的背景主要是在 setnx() 和 expire() 的方案上针对可能存在的死锁问题，做了一些优化。</p>
<h2 id="getset"><a href="#getset" class="headerlink" title="getset()"></a>getset()</h2><p>这个命令主要有两个参数 getset(key，newValue)。该方法是原子的，对 key 设置 newValue 这个值，并且返回 key 原来的旧值。假设 key 原来是不存在的，那么多次执行这个命令，会出现下边的效果：</p>
<ol>
<li>getset(key, “value1”) 返回 null 此时 key 的值会被设置为 value1</li>
<li>getset(key, “value2”) 返回 value1 此时 key 的值会被设置为 value2</li>
<li>依次类推！</li>
</ol>
<h2 id="使用步骤-1"><a href="#使用步骤-1" class="headerlink" title="使用步骤"></a>使用步骤</h2><ol>
<li>setnx(lockkey, 当前时间+过期超时时间)，如果返回 1，则获取锁成功；如果返回 0 则没有获取到锁，转向 2。</li>
<li>get(lockkey) 获取值 oldExpireTime ，并将这个 value 值与当前的系统时间进行比较，如果小于当前系统时间，则认为这个锁已经超时，可以允许别的请求重新获取，转向 3。</li>
<li>计算 newExpireTime = 当前时间+过期超时时间，然后 getset(lockkey, newExpireTime) 会返回当前 lockkey 的值currentExpireTime。</li>
<li>判断 currentExpireTime 与 oldExpireTime 是否相等，如果相等，说明当前 getset 设置成功，获取到了锁。如果不相等，说明这个锁又被别的请求获取走了，那么当前请求可以直接返回失败，或者继续重试。</li>
<li>在获取到锁之后，当前线程可以开始自己的业务处理，当处理完毕后，比较自己的处理时间和对于锁设置的超时时间，如果小于锁设置的超时时间，则直接执行 delete 释放锁；如果大于锁设置的超时时间，则不需要再锁进行处理。</li>
</ol>

        </div>
        <div class="article-copyright">
            
    <blockquote>
        <p>
            版权声明：本文为「子曰无衣」的原创文章，博客内容遵循 署名-非商业性使用-相同方式共享 协议。<br>本文永久链接是：https://wangpdizhi.gitee.io/mhexo/2020/12/22/reids%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/
        </p>
    </blockquote>


        </div>
    </article>
    

    <section id="toc-div" >
        <ol class="toc"><li class="toc-item toc-level-1"><a class="toc-link" href="#reids%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">reids分布式锁</span></a><ol class="toc-child"><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF%E9%94%81%EF%BC%9F"><span class="toc-text">什么是锁？</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF%E5%88%86%E5%B8%83%E5%BC%8F%EF%BC%9F"><span class="toc-text">什么是分布式？</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%88%86%E5%B8%83%E5%BC%8F%E5%9C%BA%E6%99%AF"><span class="toc-text">分布式场景</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BB%80%E4%B9%88%E6%98%AF%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%9F"><span class="toc-text">什么是分布式锁？</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E6%88%91%E4%BB%AC%E9%9C%80%E8%A6%81%E6%80%8E%E6%A0%B7%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%EF%BC%9F"><span class="toc-text">我们需要怎样的分布式锁？</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9F%BA%E4%BA%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E5%81%9A%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">基于数据库做分布式锁</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9F%BA%E4%BA%8E%E8%A1%A8%E4%B8%BB%E9%94%AE%E5%94%AF%E4%B8%80%E5%81%9A%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">基于表主键唯一做分布式锁</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9F%BA%E4%BA%8E%E8%A1%A8%E5%AD%97%E6%AE%B5%E7%89%88%E6%9C%AC%E5%8F%B7%E5%81%9A%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">基于表字段版本号做分布式锁</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9F%BA%E4%BA%8E%E6%95%B0%E6%8D%AE%E5%BA%93%E6%8E%92%E4%BB%96%E9%94%81%E5%81%9A%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">基于数据库排他锁做分布式锁</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BC%98%E7%BC%BA%E7%82%B9"><span class="toc-text">优缺点</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9F%BA%E4%BA%8E-Redis-%E5%81%9A%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">基于 Redis 做分布式锁</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9F%BA%E4%BA%8E-redis-%E7%9A%84-setnx-%E3%80%81expire-%E6%96%B9%E6%B3%95%E5%81%9A%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">基于 redis 的 setnx()、expire() 方法做分布式锁</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#setnx"><span class="toc-text">setnx()</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#expire"><span class="toc-text">expire()</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BD%BF%E7%94%A8%E6%AD%A5%E9%AA%A4"><span class="toc-text">使用步骤</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E5%9F%BA%E4%BA%8E-redis-%E7%9A%84-setnx-%E3%80%81get-%E3%80%81getset-%E6%96%B9%E6%B3%95%E5%81%9A%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81"><span class="toc-text">基于 redis 的 setnx()、get()、getset()方法做分布式锁</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#getset"><span class="toc-text">getset()</span></a></li><li class="toc-item toc-level-2"><a class="toc-link" href="#%E4%BD%BF%E7%94%A8%E6%AD%A5%E9%AA%A4-1"><span class="toc-text">使用步骤</span></a></li></ol></li></ol>
    </section>
    <section id="gohome" style="display: none;"><a>顶</a></section>

    <script>
        function get_top_by_link(link){
            var hnid = "#" + $(link).attr("data");
            if ($(hnid).length > 0){
                return $(hnid).offset().top;
            }else{
                return 0;
            }
        }
        //go to hn
        function gotohn(link){
            $("html,body").animate({scrollTop: get_top_by_link(link) }, 300);
        }
        //页面滚动
        function update(){
            var scrollH = $(window).scrollTop();
            if($(".toc-link")){
                $(".toc-link").each(function(i,link){
                    var mdHeight = get_top_by_link(link);
                    if(mdHeight <= scrollH + 40){
                        //高亮导航菜单
                        $('.toc-link').removeClass('on');
                        $(link).addClass('on');
                    }
                });
            }
            //返回顶部显隐
            if(scrollH < 200){
                $("#gohome").css("display","none");
            }else{
                $("#gohome").css("display","block");
            }
        }
        $(function(){
            //修复部分锚点从属关系
            if($("#toc-div >li").length > 0){
                $("#toc-div >li").appendTo($("#toc-div >ol:first"));
            }
            //返回顶部
            $('#gohome').click(function(){
                $("html,body").animate({scrollTop: 0}, 300);
                return false;
            })
            //遍历锚点
            $(".toc-link").each(function(i,link){
                $(link).attr("data",$(link).attr('href').substring(1));
                $(link).attr("href","javascript:void(0);");
                $(link).attr("onclick","gotohn(this);");
            })
            //绑定滚动事件
            $(window).bind('scroll', update);
            //初始化toc
            var first_toc = $(".toc-link")[0];
            var first_scroll = get_top_by_link(first_toc);
            var window_scroll = $(window).scrollTop();
            if(window_scroll <= first_scroll){
                $(first_toc).addClass('on');
            }
        })
    </script>

</section>
    </div>
    <footer>
    <div class="outer">
        <div class="inner">
            Powered by <a href="http://prowiki.demopage.icu/" target="_blank">ProWiki</a>
            &copy;2020 子曰无衣<br>
            <a target="_blank" rel="noopener" href="http://beian.miit.gov.cn/">冀ICP备00000000号</a>
        </div>
    </div>
</footer>

<script src="/mhexo/js/custom.js"></script>


    <script>onload_content();</script>

</body>
</html>
